package com.geek.gkfgw.web.aspect;

import com.geek.gkfgw.framework.trace.TraceContext;
import com.geek.gkfgw.framework.validator.HibernateValidator;
import com.geek.gkfgw.util.excp.GkFgwCodeEnum;
import com.geek.gkfgw.util.excp.GkFgwException;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import java.util.Date;

/**
 * <h1>Web层日志切面类。</h1><br/>
 *
 * <h1><br/><a href="https://u.geekbang.org/subject/arch3/100614201">Java 业务架构实战营：https://u.geekbang.org/subject/arch3/100614201</a></h1><br/><h1><a href="https://gitee.com/ylimhhmily/BizArchPracticeCamp">Java 业务架构实战营代码地址：https://gitee.com/ylimhhmily/BizArchPracticeCamp</a></h1><br/><h1><a href="https://u.geekbang.org/subject/java4th/1001148?source=app_share">高级 Java 工程体系课 2023 版：https://u.geekbang.org/subject/java4th/1001148?source=app_share</a></h1><br/><h1><a href="https://time.geekbang.org/column/intro/100312101?tab=catalog">Dubbo 源码剖析与实战：https://time.geekbang.org/column/intro/100312101?tab=catalog</a></h1>
 *
 * @author hmilyylimh
 * ^_^
 * @version 0.0.1
 * ^_^
 * @date 2022-02-25
 */
@Slf4j
@Order(Integer.MIN_VALUE + 10000)
@Aspect
@Component
public class WebLogAspect {

    // Ⅰ) 需要针对哪些接口进行拦截，这里将所有 web 层中所有 Controller 结尾的类全部拦截
    @Pointcut("execution (* com.geek.gkfgw.web..*Controller.* (..))")
    private void executeService() {
    }

    @Around("executeService()")
    public Object doAround(ProceedingJoinPoint pjp) throws Throwable {
        // Ⅱ) 获取一些参数变量供下面的业务逻辑使用
        Object result = null;
        String className = pjp.getTarget().getClass().getName();
        String methodName = pjp.getSignature().getName();
        Object[] params = pjp.getArgs();

        // Ⅲ) 记录执行业务逻辑的开始时间，方便后序计算业务逻辑耗时
        long startTime = System.currentTimeMillis();
        log.info("[Web.Req][{}.{}], [Params: {}]", className, methodName, params);
        try {
            // 校验参数
            validateParams(params);

            // Ⅳ) 真正的业务逻辑调用
            result = pjp.proceed();
        } catch (Throwable e) {
            // Ⅴ) 出现异常后，记录异常日志
            long cost = System.currentTimeMillis() - startTime;
            String errMsg = String.format("[Web.Error][%s.%s], [Cost: %sms], [Result: %s]", className, methodName,
                    cost, result);
            log.error(errMsg, e);

            // 异步持久化保存日志，异步持久化出现了异常不能阻碍切面逻辑流程
            asyncPersist(className, methodName, params, cost, result, TraceContext.getTraceId(), new Date());

            throw e;
        }

        // Ⅵ) 正常返回后，记录正常业务逻辑结束后的日志
        long cost = System.currentTimeMillis() - startTime;
        log.info("[Web.Resp][{}.{}], [Cost: {}ms], [Result: {}]", className, methodName, cost, result);

        // 异步持久化保存日志，异步持久化出现了异常不能阻碍切面逻辑流程
        asyncPersist(className, methodName, params, cost, result, TraceContext.getTraceId(), new Date());

        return result;
    }

    private void validateParams(Object[] args) {
        for (Object arg : args) {
            String errMsg = HibernateValidator.validate(arg);
            if (StringUtils.isNotBlank(errMsg)) {
                throw new GkFgwException(GkFgwCodeEnum.ARGS_ERROR, errMsg);
            }
        }
    }

    /**
     * <h2>异步持久化，至于是落数据库、还是落其他持久化存储工具，依每个项目的自身情况而定。</h2>
     *
     * @param className
     * @param methodName
     * @param param
     * @param cost
     * @param result
     * @param traceId
     * @param now
     */
    public void asyncPersist(String className,
                             String methodName,
                             Object param,
                             long cost,
                             Object result,
                             String traceId,
                             Date now) {
        // TODO 这里就暂时不实现是如何持久化保存日志的逻辑了
    }
}